/*
 * Copyright 2008 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package blms.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import blms.exception.UnknownVariableException;
import blms.exception.ValidationException;
import blms.util.BLMSMessages;
import blms.util.ValidatorUtil;

/**
 * <code>League</code> is the base domain object in the BLMS system. It agregates
 * <code>User</code>s, <code>Match</code>s and <code>Joining</code>s. It also exports
 * a interface to get information and relations about these objects, e.g filter matches
 * from a specific <code>User</code> and so on.   
 * 
 * @author João Arthur
 * @author Jonhnny Weslley
 * @author Thiago Emmanuel
 * @version 1.00, 21/04/2008
 * @since 1.0
 */
public class League extends Entity {

	private static final long serialVersionUID = -3084886319644495739L;

	private String name;
	private User operator;
	private Date creationDate;
	private List<Match> matches;
	private Set<Joining> joinings;
	private Expression leaguesStandingExpression;
	private Expression leaguesHandicapExpression;

	/**
	 * Creates a <code>League</code> object.
	 */
	public League() {
		// Default constructor
	}

	/**
	 * Creates a new <code>League</code>.
	 * 
	 * @param name
	 *            The name of the league.
	 * @param operator
	 *            The operator for this league.
	 * @param creationDate
	 *            The date when this league was created.
	 * @throws ValidationException
	 */
	public League(final String name, final User operator,
			final Date creationDate) {

		matches = new ArrayList<Match>();
		joinings = new HashSet<Joining>();

		this.name = name;
		this.operator = operator;
		this.creationDate = creationDate;
	}

	public void validate() throws ValidationException {
		if (ValidatorUtil.isBlankOrNull(name)) {
			throw new ValidationException(BLMSMessages.REQUIRED_DATA_MESSAGE
					+ "league name");
		}
	}

	/**
	 * Indicates whether the specified user is operator of this
	 * <code>League</code>.
	 * 
	 * @param user
	 *            the user to be verified.
	 * @return true if the specified user is the operator of this
	 *         <code>League</code>; false otherwise.
	 */
	public boolean isOperator(final User user) {
		return operator.equals(user);
	}

	/**
	 * Indicates whether the specified user is a member of this
	 * <code>League</code>.
	 * 
	 * @param user
	 *            the user which presence will be verified.
	 * @return true if the specified user is member of this <code>League</code>;
	 *         false otherwise.
	 */
	public boolean isMember(final User user) {

		if (isOperator(user)) {
			return true;
		}
		final Joining joining = getJoiningFrom(user);
		return joining != null;
	}

	/**
	 * Joins this <code>League</code> to the specified user.
	 * 
	 * @param user
	 *            the user to be inserted in this <code>League</code>.
	 * @return a <code>JoinBuilder</code> to achieve this join.
	 */
	public JoinBuilder join(final User user) {
		return new JoinBuilder(user, this);
	}

	/**
	 * Removes the specified user from this <code>League</code>.
	 * 
	 * @param user
	 *            the user to be removed from this <code>League</code>.
	 */
	public void leave(final User user) {
		final Joining joining = getJoiningFrom(user);
		joinings.remove(joining);
	}

	/**
	 * Returns a <code>List</code> of <code>Match</code> objects of the
	 * specified user.
	 * 
	 * @param user
	 *            the user whose matches must be returned.
	 * @return a <code>List</code> of <code>Match</code> objects of the
	 *         specified user.
	 */
	public List<Match> getMatchesFrom(final User user) {

		final List<Match> matchesFrom = new ArrayList<Match>();
		for (final Match match : matches) {
			if (match.isOnTheMatch(user)) {
				matchesFrom.add(match);
			}
		}

		Collections.sort(matchesFrom, Match.BY_DATE_COMPARATOR);
		return matchesFrom;
	}
	
	/**
	 * Returns a <code>List</code> of <code>Match</code> objects of the
	 * specified user, filtered from a start date
	 * 
	 * @param user
	 *            the user whose matches must be returned.
	 * @param startDate
	 *
	 * @return a <code>List</code> of <code>Match</code> objects of the
	 *         specified user.
	 */
	public List<Match> getMatchesFrom(final User user, final Date startDate) {

		List<Match> matchesFromUser = getMatchesFrom(user);
		
		if(matchesFromUser.isEmpty()) {
			return matchesFromUser;
		}
		else {
			Date endDate = matchesFromUser.get(matchesFromUser.size() -1).getMatchDate();
			return	getMatchesBetween(matchesFromUser, startDate, endDate);
		}
	}

	/**
	 * Returns a <code>Match</code> indexed by the addition order of the
	 * specified user.
	 * 
	 * @param user
	 *            the user whose matches must be returned.
	 * @param index
	 *            The positional addition order index. Starting in 1.
	 * @return a <code>Match</code> indexed by the addition order of the
	 *         specified user.
	 * @throws IndexOutOfBoundsException
	 *             if the index is out of range (<tt>index &lt; 1 || index &gt; size()</tt>)
	 */
	public Match getMatchFrom(final User user, final int index)
			throws IndexOutOfBoundsException {

		final List<Match> matchesFrom = getMatchesFrom(user);
		return getMatch(matchesFrom, index);
	}

	/**
	 * Returns a <code>Match</code> indexed by the addition order.
	 * 
	 * @param index
	 *            The positional addition order index. Starting in 1.
	 * @throws IndexOutOfBoundsException
	 *             if the index is out of range (<tt>index &lt; 1 || index &gt; size()</tt>)
	 */
	public Match getMatch(final int index) throws IndexOutOfBoundsException {

		final List<Match> matches = getMatches();
		return getMatch(matches, index);
	}

	private Match getMatch(final List<Match> matches, final int index) {

		if ((index <= 0) || (index > matches.size())) {
			throw new IndexOutOfBoundsException(BLMSMessages.INVALID_INDEX);
		}
		return matches.get(index - 1);
	}

	/**
	 * Returns a <code>List</code> of <code>Match</code> objects between the
	 * startDate and the endDate.
	 * 
	 * @param startDate
	 * @param endDate
	 * @return a <code>List</code> of <code>Match</code> objects between the
	 *         startDate and the endDate.
	 */
	public List<Match> getMatchesBetween(final Date startDate,
			final Date endDate) {
		return getMatchesBetween(matches, startDate, endDate);
	}

	private List<Match> getMatchesBetween(final List<Match> baseMatches,
			final Date startDate, final Date endDate) {

		final List<Match> filteredMatches = new LinkedList<Match>();

		for (final Match match : baseMatches) {
			final Date matchDate = match.getMatchDate();
			if ((matchDate.compareTo(startDate) >= 0)
					&& (matchDate.compareTo(endDate) <= 0)) {
				filteredMatches.add(match);
			}
		}

		Collections.sort(filteredMatches, Match.BY_DATE_COMPARATOR);
		return filteredMatches;
	}

	/**
	 * Returns a <code>Match</code> indexed by the addition order between the
	 * startDate and the endDate.
	 * 
	 * @param startDate
	 * @param endDate
	 * @param index
	 *            The positional addition order index. Starting in 1.
	 * @return a <code>Match</code> indexed by the addition order between the
	 *         startDate and the endDate.
	 * @throws IndexOutOfBoundsException
	 *             if the index is out of range (<tt>index &lt; 1 || index &gt; size()</tt>)
	 */
	public Match getMatchesBetween(final Date startDate, final Date endDate,
			final int index) throws IndexOutOfBoundsException {

		final List<Match> matchesBetween = getMatchesBetween(startDate, endDate);
		return getMatch(matchesBetween, index);
	}

	/**
	 * Returns a <code>Match</code> indexed by the addition order between the
	 * startDate and the endDate from a specific user
	 * 
	 * @param player
	 * @param startDate
	 *            the start date interval.
	 * @param endDate
	 *            the end date interval
	 * @param index
	 *            The positional addition order index. Starting in 1.
	 * @return a <code>Match</code> indexed by the addition order between the
	 *         startDate and the endDate from a specific user
	 * @throws IndexOutOfBoundsException
	 *             if the index is out of range (<tt>index &lt; 1 || index &gt; size()</tt>)
	 */
	public Match getMatchesBetween(final User player, final Date startDate,
			final Date endDate, final int index)
			throws IndexOutOfBoundsException {

		final List<Match> matchesFrom = getMatchesFrom(player);
		final List<Match> matchesBetweenFrom = getMatchesBetween(matchesFrom,
				startDate, endDate);
		return getMatch(matchesBetweenFrom, index);
	}

	/**
	 * Returns the number of wins of the specified user.
	 * 
	 * @param user
	 *            the user to be verified the number of wins.
	 * @return the number of wins of the specified user.
	 */
	public int getNumberOfWins(final User user) {
		int winners = 0;
		for (final Match match : matches) {
			if (match.isTheWinner(user)) {
				winners++;
			}
		}
		return winners;
	}

	/**
	 * Returns the number of losses of the specified user.
	 * 
	 * @param user
	 *            the user to be verified the number of losses.
	 * @return the number of losses of the specified user.
	 */
	public int getNumberOfLosses(final User user) {
		int losses = 0;
		for (final Match match : matches) {
			if (match.isTheLoser(user)) {
				losses++;
			}
		}
		return losses;
	}

	/**
	 * Returns the users of this <code>League</code>.
	 * 
	 * @return the users of this <code>League</code>.
	 */
	public Iterable<User> getUsers() {

		final Collection<User> users = new HashSet<User>();

		for (final Joining joining : joinings) {
			users.add(joining.getUser());
		}
		users.add(operator);
		return users;
	}

	/**
	 * Returns the joining between this <code>League</code> and the specified
	 * user.
	 * 
	 * @param user
	 *            the user of the joining.
	 * @return the joining between this <code>League</code> and the specified
	 *         user.
	 */
	public Joining getJoiningFrom(final User user) {

		for (final Joining joining : joinings) {

			if (joining.getUser().equals(user)) {
				return joining;
			}
		}
		return null;
	}

	/**
	 * Returns the standing of the player.
	 * 
	 * @param player
	 * @return the standing of the player.
	 */
	public int getPlayerStanding(final User player) {

		if (leaguesStandingExpression == null) {
			throw new IllegalArgumentException(
					"Undefined expression to the league " + getName());
		}
		
		try {
			return leaguesStandingExpression.evaluate(new LeagueContext(this, player));
		} catch (UnknownVariableException e) {
			throw new UnknownVariableException(BLMSMessages.UNKNOWN_STANDING_VARIABLE);
		} catch (ArithmeticException e) {
			throw new ArithmeticException(BLMSMessages.DIVISION_BY_ZERO_IN_STANDINGS);
		}
		
	}

	/**
	 * Returns the name of this <code>League</code>.
	 * 
	 * @return the name of this <code>League</code>.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Sets the name of this <code>League</code>.
	 * 
	 * @param name
	 *            the name of this <code>League</code>.
	 */
	public void setName(final String name) {
		this.name = name;
	}

	/**
	 * Returns the operator of this <code>League</code>.
	 * 
	 * @return the operator of this <code>League</code>.
	 */
	public User getOperator() {
		return operator;
	}

	/**
	 * Sets the operator of this <code>League</code>.
	 * 
	 * @param operator
	 *            the new operator of this <code>League</code>.
	 */
	public void setOperator(final User operator) {
		this.operator = operator;
	}

	/**
	 * Returns the creation date of this <code>League</code>.
	 * 
	 * @return the creation date of this <code>League</code>.
	 */
	public Date getCreationDate() {
		return creationDate;
	}

	/**
	 * Sets the creation date of this <code>League</code>.
	 * 
	 * @param the
	 *            creation date of this <code>League</code>.
	 */
	public void setCreationDate(final Date creationDate) {
		this.creationDate = creationDate;
	}

	/**
	 * Returns the matches of this <code>League</code>.
	 * 
	 * @return the matches of this <code>League</code>.
	 */
	public List<Match> getMatches() {
		return matches;
	}

	/**
	 * Sets the matches of this <code>League</code>.
	 * 
	 * @param the
	 *            matches of this <code>League</code>.
	 */
	public void setMatches(final List<Match> matches) {
		this.matches = matches;
	}

	/**
	 * Returns the <code>Joining</code>s of this <code>League</code>.
	 * 
	 * @return the <code>Joining</code>s of this <code>League</code>.
	 */
	public Set<Joining> getJoinings() {
		return joinings;
	}

	/**
	 * Returns the <code>Joining</code>s of this <code>League</code>.
	 * 
	 * @return the <code>Joining</code>s of this <code>League</code>.
	 */
	public void setJoinings(final Set<Joining> joinings) {
		this.joinings = joinings;
	}

	/**
	 * Returns the expression related to this <code>League</code>.
	 * 
	 * @return the expression related to this <code>League</code>.
	 */
	public Expression getExpression() {
		return leaguesStandingExpression;
	}

	/**
	 * Sets the expression related to this <code>League</code>.
	 * 
	 * @param the
	 *            expression related to this <code>League</code>.
	 */
	public void setExpression(final Expression expression) {
		this.leaguesStandingExpression = expression;
	}
	
	/**
	 * Gets the handicap expression of the user in this <code>Joining</code>.
	 * 
	 * @return the handicapExpression
	 */
	public Expression getHandicapExpression() {
		return leaguesHandicapExpression;
	}

	/**
	 * sets the handicap expression of the user in this <code>Joining</code>.
	 * 
	 * @param handicapExpression the handicapExpression to set
	 */
	public void setHandicapExpression(Expression handicapExpression) {
		this.leaguesHandicapExpression = handicapExpression;
	}
	
	/**
	 * @param user
	 * @return
	 */
	public int getPlayerHandicap(User user) {
		Joining joiningFrom = getJoiningFrom(user);
		return joiningFrom.getCurrentHandicap();
	}

	/**
	 * Sets the handicap value of the user.
	 * 
	 * @param user
	 * 		the user
	 * @param insertionDate
	 * 		the handicap insertion date
	 * @param handicap
	 * 		the handicap value
	 */
	public void setPlayerHandicap(User user, Date insertionDate, int handicap) {
		
		if (handicap < 0 ) {
			throw new IllegalArgumentException(BLMSMessages.INVALID_HANDICAP);
		}
		
		Joining joiningFrom = getJoiningFrom(user);
		joiningFrom.setPlayerHandicap(insertionDate, handicap);
	}

	/**
	 * @param user
	 * @return
	 */
	public int getHandicapHistorySize(User user) {
		
		if(isOperator(user)) {
			return 1;//initial handicap assignment
		}else {
			Joining joining = getJoiningFrom(user);
			return joining.getHandicapHistorySize();
		}
	}
	
	/**
	 * @param user
	 * @return
	 */
	public Date getHandicapHistoryLastUpdate(User user) {
		
		if(isOperator(user)) {
			return getCreationDate();
		}else {
			Joining joining = getJoiningFrom(user);
			return joining.getHandicapHistoryLastUpdate();
		}
	}
	
	/**
	 * Returns a <code>String</code> representation for this
	 * <code>League</code>.
	 * 
	 * @return a <code>String</code> representation for this
	 *         <code>League</code>.
	 */
	@Override
	public String toString() {
		return name;
	}
	
	/**
	 * A JoingBuilder must be used to set parameters of Joinings between an user
	 * and a league.
	 * 
	 * @author João Arthur
	 * @author Jonhnny Weslley
	 * @author Thiago Emmanuel
	 * 
	 */
	public static class JoinBuilder {

		private final User joiningUser;
		private final League joiningLeague;
		private final Serializable joiningId;
		private int initialHandicap;

		/**
		 * Constructs a new JoinBuilder that will be used to join the specified
		 * user and the league.
		 * 
		 * @param user
		 *            the user to be joined with the specified league.
		 * @param league
		 *            the league to be joined with the specified user.
		 */
		private JoinBuilder(final User user, final League league) {
			joiningUser = user;
			joiningLeague = league;
			joiningId = league.joinings.size();
		}

		/**
		 * Sets the initial handicap for the Joining between the user and the
		 * league.
		 * 
		 * @param handicap
		 *            the initial handicap.
		 * @return this <code>JoinBuilder</code>.
		 */
		public JoinBuilder withHandicap(final int handicap) {
			initialHandicap = handicap;
			return this;
		}

		/**
		 * Sets the date when the Joining was done.
		 * 
		 * @param date
		 *            the date when the Joining was done.
		 * @return the Joining between the user and the league.
		 */
		public Joining on(final Date date) {
			Joining joining = new Joining(joiningUser, joiningLeague, date, initialHandicap);
			joining.setId(joiningId);
			joiningLeague.joinings.add(joining);
			return joining;
		}
	}

}